/**************************************************************************
 *
 *   Copyright 2010 American Public Media Group
 *
 *   This file is part of AIR2.
 *
 *   AIR2 is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   AIR2 is distributed in the hope that it will be useful, 
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License 
 *   along with AIR2.  If not, see <http://www.gnu.org/licenses/>.
 *
 *************************************************************************/

/* AIR2 Search UI */
Ext.ns('AIR2.Search');
Ext.ns('AIR2.Search.Facets');
Ext.ns('AIR2.Search.State');
Ext.ns('AIR2.Search.SavedSearch');

// global var to hold the Search.Query object for the request
// TODO populate this from the actual requested query
AIR2.Search.Query = new Search.Query.Dialect();

Ext.Ajax.timeout = 120000; // default is 30sec. we give it 2 minutes for facets.

Ext.onReady(function() {
    AIR2.Search.BIGSPINNER          = AIR2.HOMEURL + '/lib/extjs/resources/images/default/shared/large-loading.gif';
    AIR2.Search.LOADING_IMG         = '<img src="'+AIR2.Search.BIGSPINNER+'"/>';
    AIR2.Search.BALLSPINNER         = AIR2.HOMEURL + '/lib/extjs/resources/images/default/shared/loading-balls.gif';
    AIR2.Search.BALLSPINNER_IMG     = '<img src="'+AIR2.Search.BALLSPINNER+'"/>';
    AIR2.Search.SPINNER             = AIR2.HOMEURL + '/css/img/loading.gif';
    AIR2.Search.SPINNER_IMG         = '<img src="'+AIR2.Search.SPINNER+'" align="top" />';
    AIR2.Search.TOGGLEADVSRCH       = 'Hide Advanced';
    AIR2.Search.PLUS_IMG            = AIR2.HOMEURL + '/lib/extjs/resources/images/default/dd/drop-add.gif';
    AIR2.Search.MINUS_IMG           = AIR2.HOMEURL + '/lib/extjs/resources/images/default/dd/delete.gif';
    AIR2.Search.DROP_IMG            = AIR2.HOMEURL + '/lib/extjs/resources/images/default/tree/drop-under.gif';
    AIR2.Search.CHECKED_IMG         = AIR2.HOMEURL + '/lib/extjs/resources/images/default/menu/checked.gif';
    AIR2.Search.UNCHECKED_IMG       = AIR2.HOMEURL + '/lib/extjs/resources/images/default/menu/unchecked.gif';
    AIR2.Search.TREE_PLUS_IMG       = AIR2.HOMEURL + '/lib/extjs/resources/images/default/tree/elbow-plus-nl.gif';
    AIR2.Search.TREE_MINUS_IMG      = AIR2.HOMEURL + '/lib/extjs/resources/images/default/tree/elbow-minus-nl.gif';
    AIR2.Search.BULLET_IMG          = AIR2.HOMEURL + 'img/search/unchecked.gif';
    AIR2.Search.BULLET_INCLUDE_IMG  = AIR2.HOMEURL + 'img/search/tick.gif';
    AIR2.Search.BULLET_EXCLUDE_IMG  = AIR2.HOMEURL + 'img/search/cross.gif';
    AIR2.Search.LINK_IMG            = AIR2.HOMEURL + 'img/search/link.gif';

    AIR2.Search.setupHistory();
});

AIR2.Search.decodeUrlHash = function(hash) {
    var hashParams = Ext.urlDecode(hash);
    var state = {};
    Ext.iterate(hashParams, function(key,val,obj) {
        state[key] = Ext.decode(val);
    });
    return state;
}

AIR2.Search.setupHistory = function() {
    Ext.History.init();

    AIR2.Search.Facets.CheckOnLoad = [];
    
    // Handle this change event in order to restore the UI to the appropriate history state
    Ext.History.on('change', function(token){
        //Logger("history token: ", token);
        if (token) {
            //Logger("token true");
            if (AIR2.Search.STATE != token) {
                var before = AIR2.Search.decodeUrlHash(AIR2.Search.STATE);
                var after  = AIR2.Search.decodeUrlHash(token);
                //Logger("TODO update facet checkboxes and store");
                //Logger("before:"+Ext.encode(before));
                //Logger("after :"+Ext.encode(after));
                if (Ext.isDefined(after['facets'])) {
                    // toggle check on whatever changed
                    var beforeIds = AIR2.Search.Facets.getDomIds(before['facets']);
                    var afterIds  = AIR2.Search.Facets.getDomIds(after['facets']);
                    Ext.each(beforeIds, function(item,idx,array) {
                        //Logger("before:"+item);
                        if (afterIds.indexOf(item) == -1) {
                            //Logger("before "+item+" not present in after -- must have been clicked");
                            var el = Ext.get(item);
                            if (!el) {
                                Logger("no el for "+item);
                                AIR2.Search.Facets.CheckOnLoad.push(item);
                                return;
                            }
                            el.dom.checked = false;
                            AIR2.Search.Facets.updateResults(el.dom);
                        }
                    });
                    Ext.each(afterIds, function(item,idx,array) {
                        //Logger("after:"+item);
                        if (beforeIds.indexOf(item) == -1) {
                            //Logger("after "+item+" not present in before -- must have been clicked");
                            var el = Ext.get(item);
                            if (!el) {
                                Logger("no el for "+item);
                                AIR2.Search.Facets.CheckOnLoad.push(item);
                                return;
                            }
                            el.dom.checked = true;
                            AIR2.Search.Facets.updateResults(el.dom);
                        }
                    });
                }
            }
            AIR2.Search.STATE = token;
        }
        else {
            // This is the initial default state.  Necessary if you navigate starting from the
            // page without any existing history token params and go back to the start state.
            //Logger("no history token");
            if (AIR2.Search.STATE) {
                Logger("no history now but had state:", AIR2.Search.decodeUrlHash(AIR2.Search.STATE));
                var before = AIR2.Search.decodeUrlHash(AIR2.Search.STATE);
                if (Ext.isDefined(before['facets'])) {
                    var beforeIds = AIR2.Search.Facets.getDomIds(before['facets']);
                    Ext.each(beforeIds, function(item,idx,array) {
                        var el = Ext.get(item);
                        if (!el) { 
                            Logger("no el for "+item);
                            AIR2.Search.Facets.CheckOnLoad.push(item);
                            return; 
                        }       
                        el.dom.checked = false;
                        AIR2.Search.Facets.updateResults(el.dom);
                    });
                }
                AIR2.Search.STATE = null;
            }    
        }
    });

    var locHash = AIR2.Search.getLocationHash();
    if (locHash && locHash.length) {
        Ext.History.fireEvent('change', locHash);
    }
}

AIR2.Search.Facets.getDomIds = function(facets) {
    var ids = [];
    Ext.iterate(facets, function(field,val,obj) {
        Ext.iterate(val, function(value,one,obj2) {
            var id = field+'-'+value;
            ids.push(id);
        });
    });
    return ids;
}

AIR2.Search.getStart = function() {
    var start = AIR2.Search.PARAMS['o'] !== null
        ? parseInt(AIR2.Search.PARAMS['o'])
        : 0;
    return start;
}

AIR2.Search.getPageSize = function() {
    var pageSize = AIR2.Search.PARAMS['p'] !== null
        ? parseInt(AIR2.Search.PARAMS['p'])
        : 100; // TODO get default page size from session.prefs
    return pageSize;
}

AIR2.Search.getQuery = function() {
    var q = AIR2.Search.PARAMS['q'];
    var state = AIR2.Search.getState();
    if (state && state['facets'] && AIR2.Search.State.toQuery(state).length) {
        q = '('+q+') AND ' + AIR2.Search.State.toQuery(state);
    }
    //Logger("q=",q);
    return q;
}

AIR2.Search.State.toQuery = function(state) {
    var q = [];
    if (!Ext.isDefined(state['facets'])) {
        return;
    }
    //Logger("facets:",state['facets']);
    Ext.iterate(state['facets'], function(field,val,obj) {
        var dialect = new Search.Query.Dialect();
        Ext.iterate(val, function(value,one,obj2) {
            var clause = new Search.Query.Clause({
                field : field,
                value : value,
                op    : '=',
                quote : value.match(/^\(/) ? '' : '"'
            });
            dialect['or'].push(clause);
        });
        q.push(new Search.Query.Clause({op:'()', field:field, value:dialect}));
    });
    var query = new Search.Query.Dialect({and:q});
    return query.stringify();
}

AIR2.SearchPanel = function(cfg) {
        
    if (!Ext.isDefined(cfg.searchUrl)) {
        throw new Ext.Error("searchUrl required");
    }
    
    if (!Ext.isDefined(cfg.title)) {
        cfg.title = 'Results';
    }
    
    var pageSize = AIR2.Search.getPageSize();
    var start = AIR2.Search.getStart();
    var query = AIR2.Search.getQuery();
    var state = AIR2.Search.getState();
        
    //Logger("pageSize,start=",pageSize,start);
    
    var initialParams = {};
    Ext.iterate(AIR2.Search.PARAMS, function(k,v,obj) {
        if (v !== null) {
            initialParams[k] = v;
        }
    });
    Ext.apply(initialParams, {
        q: query,
        start: start,
        limit: pageSize
    });
    var resultsStore = new Ext.data.JsonStore({
        autoLoad        : {params:initialParams},
        restful         : true,
        url             : cfg.searchUrl,
        listeners : {
            beforeload : function(thisStore, opts) {
                //Logger(opts);
                if (opts.params['sort'] == "uri") {
                    //Logger('change sort == rank');
                    opts.params['sort'] = "rank";
                }
            },
            load: function(thisStore, recs, opts) {
                //Logger("resultsStore.load:", arguments);
                Ext.getCmp('air2-search-results').setTotal(thisStore.getTotalCount(), thisStore.reader.jsonData['unauthz_total']);
                //Logger(thisStore);
                AIR2.Search.REQ_QUERY = Ext.decode(thisStore.reader.jsonData['json_query']);
                AIR2.Search.REQ_BOOL  = 'AND'; // TODO thisStore.reader.jsonData['TODO'];
            },
            exception: function(proxy, type, action, options, response, args) { 
                Logger("ResultsStore caught exception:", type, action, response, args);
                AIR2.Search.showError(response, options);
            }
        
        }
        
    });

    var queryTitle = AIR2.Search.QUERY;
    if (!queryTitle.length) {
        queryTitle = "All " + cfg.title;
    }
    
    // TODO add flavor based on current query type (sources, projects, etc)
    var advSearch = new AIR2.UI.Panel({
        title: 'Advanced Search',
        id: 'air2-advsearch-panel',    
        colspan: 3,
        hidden: true, // TODO optional via url param? see magicToggle
        style: 'margin-bottom:10px',
        magicToggle: function() {
            //Logger("magicToggle");
            var el = this.el;
            var grid = this;
            if (this.hidden) {
                this.show();
                el.slideIn('t', { 
                    duration: 0.25,
                    callback: function() {
                        // if there isn't yet a rule (as the first time we show)
                        // then add one. the 'afterrender' listener is buggy
                        // on the formpanel itself.
                        var fPanel = Ext.getCmp(AIR2.Search.Advanced.FORMID);
                        var nClauses = fPanel.getClauses().length;
                        if (!nClauses) {
                            // because our search is async, this might not be ready at the time
                            // this panel is initialized.
                            if (AIR2.Search.REQ_QUERY) {
                                // TODO dialect.walk() to add clauses
                                //var dialect = Ext.getCmp(AIR2.Search.Advanced.EXPLAIN);
                                //dialect.fromJson(AIR2.Search.REQ_QUERY);
                                //Logger(dialect.toString());
                                fPanel.addClause(); 
                            }
                            else {
                                fPanel.addClause();
                            }
                        }
                    }
                });
            }
            else {
                el.slideOut('t', {
                    duration: 0.25,
                    callback: function() { 
                        grid.hide() 
                    }
                });
            }

        },
        items: [
            AIR2.Search.Advanced.Panel({
                current: queryTitle
            })
        ]
    });
    
    AIR2.Search.DATAVIEW = new AIR2.UI.DataView({
        store           : resultsStore,
        tpl             : cfg.resultTpl,
        multiSelect     : true,
        overCls         : 'air2-search-result-over',
        selectedClass   : 'air2-search-result-selected',
        itemSelector    : 'div.air2-search-result',
        trackOver       : true,
        baseRankNum     : start + 1,
        
        // private override
        onBeforeLoad    : function(){
            this.clearSelections(false, true);
            this.getTemplateTarget().update('<div class="air2-loading-wrap"><div class="air2-loading-img"></div></div>');
            this.all.clear();
        },

        prepareData     : function(data, num, rec) {
            data.rank = num + this.baseRankNum;
            return data;
        },

        listeners       : {
            selectionchange : function(thisDv, selections) {
                //Logger("selections:", selections);
            
            },
            
            beforeselect : function(thisDv, node, curSelected) {
                //Logger("beforeselect:",node);
            
                return true;
            },
            
            render: function(thisDv) {
                var air2app = Ext.getCmp('air2-app');
                thisDv.dragZone = new AIR2.Drawer.DragZone(thisDv.getTemplateTarget(), {
                    getDragData: function(event) {
                        var el = event.getTarget(thisDv.itemSelector, 10);

                        if (el) {
                            var ddObj = {
                                repairXY: [Ext.fly(el).getX(), event.getXY()[1]]
                            };

                            switch (AIR2.Search.IDX) {
                                case 'sources':
                                case 'active-sources':
                                case 'responses':
                                case 'activities':
                                    ddObj.ddBatchType = 'S';
                                    break;
                                default:
                                    return false; //disable D&D
                                    break;  // can't get here
                            }

                            if (thisDv.allSelected) {
                                ddObj.ddel = thisDv.store.getTotalCount()+' '+AIR2.Search.IDX;
                                ddObj.selections = {
                                    i: AIR2.Search.IDX,
                                    q: AIR2.Search.QUERY,
                                    total: thisDv.store.getTotalCount()
                                }
                            }
                            else {
                                ddObj.ddel = thisDv.getSelectionCount()+' '+AIR2.Search.IDX;
                                ddObj.selections = function() {
                                    var selections = [];
                                    var recs = thisDv.getSelectedRecords();
                                    Ext.each(recs, function(item, idx, array) {
                                        selections.push(item.data.uri);  // TODO if uri is *not* src_uuid..??
                                    });
                                    return selections;
                                };
                            }

                            return ddObj;
                        }
                        return false; //default: disable D&D
                    }
                });
            },
            mouseenter : function(thisDv,idx,node,e) {
                Ext.get(node).toggleClass('air2-search-result-hover');
            },
            mouseleave : function(thisDv,idx,node,e) {
                Ext.get(node).toggleClass('air2-search-result-hover');
            }
        }
    });

    var allBtn = new AIR2.UI.Button({
        air2type: 'BLUE',
        air2size: 'MEDIUM',
        text: 'Select All',
        enableToggle: true,
        toggleHandler: function(btn, pressed) {
            // only allow clicking if results > 0
            if (AIR2.Search.DATAVIEW.store.getCount() > 0) {
                // add and remove listeners based on 'allSelected' state
                if (pressed) {
                    AIR2.Search.DATAVIEW.allSelected = true;
                    AIR2.Search.DATAVIEW.select(AIR2.Search.DATAVIEW.getNodes());
                    AIR2.Search.DATAVIEW.on('beforeclick', btn.dvBlockClicks);
                    AIR2.Search.DATAVIEW.on('selectionchange', btn.dvSelectionChange);
                }
                else {
                    AIR2.Search.DATAVIEW.allSelected = false;
                    AIR2.Search.DATAVIEW.un('beforeclick', btn.dvBlockClicks);
                    AIR2.Search.DATAVIEW.un('selectionchange', btn.dvSelectionChange);

                    // if there is one selection, change to it ... else none
                    var nodes = AIR2.Search.DATAVIEW.getNodes();
                    nodes = (nodes.length == 1) ? nodes : [];
                    AIR2.Search.DATAVIEW.select(nodes);
                }
            }
            else {
                btn.toggle(false);
            }
        },
        /* Listener to block shift/ctrl clicking while 'allSelected' */
        dvBlockClicks: function(dv, idx, node, ev) {
            if (ev.shiftKey || ev.ctrlKey) return false;
        },
        /* Listener to unselect-all when a single node is clicked */
        dvSelectionChange: function(dv, sel) {
            if (dv.allSelected && sel.length == 1) {
                allBtn.toggle(false);
                dv.select(sel);
            }
        }
    });
    var buttons = [
        '->',
        allBtn,
        ' ',
        {
            xtype: 'air2button',
            air2type: 'BLUE',
            air2size: 'MEDIUM',
            text: 'Save Search',
            handler: function() {
                AIR2.Search.Save();
            }
        }
    ];
    
    // TODO for now only sources has advanced search
    if (    AIR2.Search.IDX == 'sources' 
        ||  AIR2.Search.IDX == 'active-sources'
    ) {
        var adv = {
            xtype: 'air2button',
            air2type: 'BLUE',
            air2size: 'MEDIUM',
            text: 'Advanced',
            enableToggle: true,
            toggleHandler: function(btn, pressed) {
                advSearch.magicToggle();
            }
        };
        buttons.splice(1, 0, adv, ' ');
    }
    
    var resultsPanel = new AIR2.UI.Panel({
        colspan : 2,
        title   : cfg.title,
        iconCls : 'air2-icon-'+AIR2.Search.IDX,
        id      : 'air2-search-results',
        items   : AIR2.Search.DATAVIEW,
        tools   : buttons,
        fbar: new Ext.PagingToolbar({
            cls: 'air2-search-pager',
            store: resultsStore,  // dataview and PagingToolbar using same store
            displayInfo: true,
            pageSize: pageSize,
            prependButtons: true,
            items: [
            
            ],
            listeners : {
                beforechange : function(thisPager, opts) {

                // load each page with an actual URI, rather than in-place
                    var params = {
                        q: AIR2.Search.QUERY,
                        o: opts.start,
                        p: opts.limit
                    };
                    // TODO facets, etc.?
                    var uri = AIR2.Search.URL+'/'+AIR2.Search.IDX+'?'+Ext.urlEncode(params);
                    //Logger("page:", uri);
                    window.location = uri;
                    return false;

                }
            }
        })
    });

    var facetsPanel = new AIR2.Search.Facets.WrapperPanel({
        facetsUrl: cfg.searchUrl+'&'+Ext.urlEncode({f:1,r:0,q:AIR2.Search.getQuery()}),
        facetDefs: cfg.facetDefs,
        idx      : AIR2.Search.IDX
    });
    
    // the footer monkeybusiness is to preserve the rounded
    // corners on the panel bottom, which seem to disappear
    // in some browsers because the body is modified with
    // the actual facets, after the panel has been rendered.
    var filtersPanel = new AIR2.UI.Panel({
        colspan : 1,
        title   : 'Filter by',
        id      : 'air2-search-filters',
        iconCls : 'air2-icon-filter',
        style: 'padding-bottom: 10px',
        items   : [
            facetsPanel
        ]
    });
    
    var silos = AIR2.Search.siloLinks(cfg);

    var siloLinks = new AIR2.UI.Panel({
        colspan : 1,
        //title   : 'Silos',
        cls     : 'air2-search-silos',
        noHeader: true,
        items   : silos
    });
    
    // TODO this seems brittle
    var thisUrl = AIR2.Search.URL+'/'+AIR2.Search.IDX+'?'+Ext.urlEncode({q:AIR2.Search.QUERY});
    var siloPanel = new AIR2.UI.Panel({
        title : 'Category',
        colspan: 1,
        cls    : 'air2-search-advpanel',        
        items  : [
            siloLinks
        ]
    });
    
    var searchPanel = new AIR2.UI.PanelGrid({
        columnLayout: '12',
        columnWidths: [.25,.75],
        items: [
            siloPanel,
            resultsPanel,
            filtersPanel
        ]
    });

    return [advSearch, searchPanel]; 
}

/*
// TODO a lot of this saved search UI work 
// is probably already present in other .js classes.
// refactor to re-use that code.
*/
AIR2.Search.Save = function() {

    // a saved seach is just a named bookmark stored in the db,
    // since everything about a search should be in the URL
    var pattern = /\/search\/(\w+)\/?\?(.+)/;
    var url = top.location.href.match(pattern);
    var saved_search = {
        idx: url[1],
        params: url[2]
    }
    //Logger("save search:", saved_search);
    AIR2.Search.SavedSearch.Dialog(saved_search);
}

// TODO other validation?
AIR2.Search.SavedSearch.TextField = function() {
    var field = new AIR2.UI.RemoteText({
        fieldLabel  : 'Name',
        name        : 'ssearch_name',
        minChars    : 2,
        maxLength   : 255,
        width       : 300,
        remoteUrl   : AIR2.HOMEURL + '/savedsearch/name.json'
    });
    return field;
}

AIR2.Search.SavedSearch.Store = function() {

    var store = new Ext.data.JsonStore({
        autoSave: false,
        url: AIR2.HOMEURL +'/savedsearch.json',
        restful: true,  // use GET where appropriate
        root: 'radix',
        idProperty: 'ssearch_uuid',
        fields: ['ssearch_name','ssearch_uuid','ssearch_public_flag','ssearch_params'],
        writer: new Ext.data.JsonWriter({encode: true, writeAllFields: false})
    });
    var mask = new Ext.LoadMask(Ext.getBody(), {msg:'Saving...'});
    store.on('beforewrite', function(theStore, act, rs, opts, arg) {
        //Logger("beforewrite:", act, rs, opts, arg);
        mask.show();
        return true;
    });
    store.on('exception', AIR2.Search.AjaxExceptionHandler);
    store.on('save', function(theStore, batch, data) {
        mask.hide();
        //Logger('record saved:', batch, data);
        if (Ext.isDefined(data.create)) {
            if (!data.create.length || data.create[0] === null) {
                //Logger("looks like a problem");
                return; // do not close window
            }
            else {
                
            }
        }
        AIR2.Search.SavedSearch.Window.close();
    });
    
    return store;
}

AIR2.Search.SavedSearch.Dialog = function(saved_search) {
    var jsonStr, textField, btnHandler, ssForm;
    jsonStr = Ext.encode(saved_search);
    textField = AIR2.Search.SavedSearch.TextField();
    btnHandler = function(btn, ev) {
        if (!textField.isValid()) {
            textField.showError("Saved Search Error", "Must enter a valid unique name.");
            return;
        }
        btn.disable();  // prevent double-click
        var searchName = textField.getValue();
        var isPublic = 0;
        var radios = ssForm.items.find(function(item) {
            if (item.getXType() == 'radiogroup') {
                //Logger(item);
                var sel = item.getValue();
                //Logger(sel.getGroupValue());
                isPublic = parseInt(sel.getGroupValue());
                return true;
            }       
        });     
        var ssStore = AIR2.Search.SavedSearch.Store();
        var ss = {
            'ssearch_params'      : jsonStr,
            'ssearch_name'        : searchName,
            'ssearch_public_flag' : isPublic
        };
        var ssRec = new ssStore.recordType(ss);
        ssStore.add(ssRec);
        ssStore.save();
    };
    ssForm = new Ext.form.FormPanel({
        layout: 'form',
        style: 'padding: 8px',
        labelWidth: 60,
        width: 500,
        border: false,
        frame: false,
        items : [
            textField,
            {
                xtype : 'radiogroup',
                columns: 1,
                cls: 'save-search-radios',
                items : [
                    {boxLabel:'Private', checked: true, name: 'ssearch_public_flag', inputValue: '0'},
                    {boxLabel:'Shared',  name: 'ssearch_public_flag', inputValue: '1'}
                ]
            }
        ],
        buttons: [
            {
                xtype: 'air2button',
                air2type: 'SAVE',
                air2size: 'MEDIUM',
                text: 'Save',
                handler: btnHandler
            },
            {
                xtype: 'air2button',
                air2type: 'CANCEL',
                air2size: 'MEDIUM',
                text: 'Cancel',
                handler: function() { AIR2.Search.SavedSearch.Window.close() }
            }
        ]
    });
    
    AIR2.Search.SavedSearch.Window = new AIR2.UI.Window({
        title : 'Save Search',
        modal : true,
        items : [ssForm],
        width : 400,
        height: 175
    });
    AIR2.Search.SavedSearch.Window.show();

}

AIR2.Search.AjaxExceptionHandler = function(proxy, type, action, options, response, args) { 
/*
    Logger("generic Ajax handler caught exception");
    Logger("proxy:", proxy);
    Logger("type:", type);
    Logger("action:", action);
    Logger("options:", options);
    Logger("response:", response);
    Logger("args:", args);
*/

    if (!response) {
        Logger("no response object");
        return; 
    }
    var resp;
    if (response.status != '404' && response && response.responseText) {
        try {   
            resp = Ext.decode(response.responseText);
        }       
        catch (err) { 
            resp = { message: "Ext.decode failed: " + err.message };
        }       
    }
    else {  
        resp = { message: "Problem contacting the server." };
    }
    //Logger(resp);
    alert("Error: " + resp.message);
}


AIR2.Search.siloLinks = function(cfg) {

    // TODO move these to a config file
    var altCats = {
        'inquiries'         : 'Queries',
        'sources'           : 'All Sources',
        'active-sources'    : 'Active Sources',
        'responses'         : 'Submissions',
//        'activities'      : 'Activities',  // disabled per team meeting decision 15 Nov 2010
        'projects'          : 'Projects'
    };

    var filterTpl = new Ext.XTemplate(
        '<div class="air2-silolink air2-corners">',
        '<a class="air2-icon {iconCls}" href="{href}">{label}</a>&nbsp;',
        '(<span id="{elId}">',AIR2.Search.BALLSPINNER_IMG,'</span>)',
        '</div>'
    );
    var currentTpl = new Ext.XTemplate(
        '<div class="air2-silolink air2-corners-left air2-silolink-current" style="width:500px">',
        '<a class="air2-icon {iconCls}" href="{href}">{label}</a>&nbsp;',
        '(<span id="{elId}">',AIR2.Search.BALLSPINNER_IMG,'</span>)',
        '</div>'
    );
    filterTpl.compile();
    currentTpl.compile();
    
    var silos = [
        new Ext.Component({
            html: '<div class="air2-silolink-top"></div>'
        })
    ];
    var sorter = function(a,b) {
        var aLabel = altCats[a].toLowerCase();
        var bLabel = altCats[b].toLowerCase();
        if (aLabel < bLabel) { return -1 }
        if (aLabel > bLabel) { return 1  }
        return 0;
    };
    var altNames = [];
    Ext.iterate(altCats, function(key,label,obj) {
        altNames.push(key);
    });
    
    Ext.each(altNames.sort(sorter), function(item,idx,array) {
        var key,label,obj;
        key = item;
        label = altCats[key];
        var tpl = filterTpl;
        var url = AIR2.Search.URL+'.json?'+
            Ext.urlEncode({q:Ext.util.Format.htmlDecode(AIR2.Search.QUERY), i:key});
        var href = AIR2.Search.URL+'/'+key+'?'+
            Ext.urlEncode({q:Ext.util.Format.htmlDecode(AIR2.Search.QUERY)});
        var elId = 'air2-'+key+'-count';
        var listeners = {
            render : function(p) {
                var el = p.getEl();
                el.on('mouseenter', function() { el.toggleClass('air2-silo-over'); });
                el.on('mouseleave', function() { el.toggleClass('air2-silo-over'); });
                el.on('click', function() { window.location = el.child('a').dom.href; });
                AIR2.Search.getCount({
                    url : url,
                    elId: elId
                    //redirectOnError: AIR2.Search.URL+'/help?'+Ext.urlEncode({q:AIR2.Search.QUERY})
                }); 
            }
        };
        if (key === AIR2.Search.IDX) {
            tpl = currentTpl;
            listeners['afterrender'] = function(p) { p.getEl().setWidth(400) };   // something bigger than parent
            //url = cfg.searchUrl+'&c=1'; // TODO include whatever facets were applied to actual search
        }

        var silo = new Ext.Component({
            tpl   : tpl,
            data  : {
                href: href,
                elId: elId,
                label: label,
                iconCls: 'air2-icon-'+key
            },
            border: false,
            listeners: listeners
        });

        silos.push(silo);
    });
    
    return silos;
}

/**********************************************************************
 * @class AIR2.Search.getCount
 * 
 * Expects a single "cfg" object as argument, with two values:
 * "url" and "elId". Will fire off a Ajax.request to the "url"
 * and update the element with "elId" with the total count
 * in the response.
 */

AIR2.Search.getCount = function(cfg) {

    var setter = function(json) {
    
        if (json && Ext.isDefined(json.total) && cfg.elId) {
            var el = Ext.get(cfg.elId);
            if (el) {
                el.dom.innerHTML = json.total;
            }
        }
    
    };

    // fetch the query count, optionally updating the DOM
    Ext.Ajax.request({
        url: cfg.url + '&c=1',
        success: function( resp, opts ) {
            //Logger(resp);
            var json = Ext.decode(resp.responseText);
            //Logger(cfg, json.total);
            setter(json);
            if (cfg.callback) {
                cfg.callback(json);
            }
        },
        failure: function( resp, opts ) {
            //Logger("Count failure: ", resp);
            setter({total:0});
            if (!AIR2.Search.ErrorInCount) {
            
                // it's very common for "no such field" errors
                // to happen because tag defs are not the same across
                // all idx silos.
                if (resp.responseText.match(/no such field/i)) {
                    Logger(resp.responseText);
                    return;
                }
            
                AIR2.Search.showError(resp, opts);
            }
            AIR2.Search.ErrorInCount = true;
            Ext.get(cfg.elId).dom.innerHTML = '0';
            // optionally redirect to help page
            if (cfg.redirectOnError) {
                window.location = cfg.redirectOnError;
            }
        }
    });

}

AIR2.Search.showError = function(response, opts) {
    // because we may have fired several requests for count
    // we only want to show the first error.
    // TODO how to unset this?
    if (AIR2.Search.ErrorInProgress) {
        return;
    }
    AIR2.Search.ErrorInProgress = true;
    if (!response) {
        Logger("no response object");
        return; 
    }       
    var resp;
    if (response.status != '404' && response && response.responseText) {
        try {   
            resp = Ext.decode(response.responseText);
        }       
        catch (err) { 
            resp = { error: "Ext.decode failed: " + err.message };
        }       
    }       
    else {
        Logger("XHR error: ", response);
        return; // often from clicking away from a page while XHR in progress.
    }       
    Logger("Error:", resp);
    if (resp.error) {
        alert(resp.error);
    }
    else {
        alert("Unknown server error");
    }
}

AIR2.Search.Facets.WrapperPanel = Ext.extend(Ext.Panel, {
    autoHeight: true,
    border: false,
    id: 'air2-search-facets',
    cls: 'air2-corners',
    initComponent : function(cfg) {
        if (!Ext.isDefined(this.facetsUrl)) {
            throw new Ext.Error("facetsUrl not defined");
        }
        if (!Ext.isDefined(this.facetDefs)) {
            throw new Ext.Error("facetDefs not defined");
        }
        
        this.baseUrl = AIR2.Search.URL + '/' + this.idx + '?';

        // fetch the facets and add them as items.
        
        this.on('afterrender', function() {
            var panel = this;
            var el = panel.getResizeEl();
            //Logger('render Facets panel:', el);
            var myMask = new Ext.LoadMask(el, { 
                msg: 'Fetching facets...'
            });
            myMask.show();
            //Logger("myMask: ", myMask);
            Ext.Ajax.request({
                url: panel.facetsUrl,
                success: function(resp,opts) {
                    myMask.hide();
                    panel.addFacets(resp,opts);
                },
                failure: function( resp, opts ) {
                    myMask.hide();
                    Logger("facets failure: ", resp);
                    AIR2.Search.showError(resp, opts);
                }
            });
        });

        this.defaults = { style: 'padding: 2px', border: false };

        AIR2.Search.Facets.WrapperPanel.superclass.initComponent.call(this,cfg);
    },

    getClearButton: function() {
        return this.getTopToolbar().get(2);
    },
    
    addFacets : function(resp, opts) {
        var fp = this; 
        var json = Ext.decode(resp.responseText);
        //Logger("addFacets");
        //Logger("facets:", json.facets);
        var facetPanels = [];
        var genericTpl = new Ext.XTemplate(
            '<div class="air2-facet-pair">',
            '<input onclick="{onclick}" type="checkbox" id="{facetId}" name="{facetName}" {checked}/>',
            '<label for="{facetId}" class="{facetValue}"><span class="air2-facet-value">{label}</span>',
            ' <span class="count">({count})</span></label>',
            '</div>'
        );
        genericTpl.compile();
        //Logger(fp);
        // sort them by facetDefs.label
        var facetOrder = [];
        if (Ext.isDefined(fp.facetDefs['sortOrder'])) {
            facetOrder = fp.facetDefs['sortOrder'];
        }
        else {
            var facetNames = [];
            Ext.iterate(fp.facetDefs, function(name,def,obj) {
                facetNames.push(name);
            });
            var sorter = function(a,b) {
                var aLabel = fp.getFacetLabel(a).toLowerCase();
                var bLabel = fp.getFacetLabel(b).toLowerCase();
                if (aLabel < bLabel) { return -1 }
                if (aLabel > bLabel) { return 1  }
                return 0;
            };
            facetOrder = facetNames.sort(sorter);
        }

        Ext.each(facetOrder, function(name,idx,array) {
            var facets;
            if (!Ext.isDefined(json.facets[name])) {
                Logger("not returned from server, skipping facet", name);
                return true;    // Response does not define this facet
            }
            facets = json.facets[name];
            var listOfFacets = [];
            if (facets.length > 200) { // TODO arbitrary limit
                Logger("too long ("+facets.length+"), skipping facet", name);
                return true;
            }
            var tpl;
            var isActive = false;
            if (fp.getFacetRenderer(name)) {
                var r = fp.getFacetRenderer(name);
                listOfFacets = r.render(name,facets,obj);
            }
            else {
            
                // default is sorted by facet label
                var sorter = function(a,b) {
                    var aLabel = fp.getItemLabel(name,a).toLowerCase();
                    var bLabel = fp.getItemLabel(name,b).toLowerCase();
                    if (aLabel < bLabel) { return -1 }
                    if (aLabel > bLabel) { return 1  }
                    return 0;
                };
            
                Ext.each(facets.sort(sorter), function(item,idx,array) {
                    var checkedState = '';
                    var elId = Ext.util.Format.htmlEncode(name+'-'+item.term);
                    if (AIR2.Search.Facets.CheckOnLoad.indexOf(elId) != -1) {
                        checkedState = 'checked="true"';
                        isActive = true;
                    }
                    listOfFacets.push({
                        cls: 'air2-facet-body',
                        tpl: genericTpl,
                        data: {
                            label       : Ext.util.Format.htmlEncode(fp.getItemLabel(name, item)),
                            count       : item.count,
                            facetName   : Ext.util.Format.htmlEncode(name),
                            facetValue  : Ext.util.Format.htmlEncode(item.term),
                            facetId     : elId,
                            href        : fp.uriFor(name, item.term),
                            checked     : checkedState,
                            onclick     : 'AIR2.Search.Facets.onFacetClick(this,true)'
                        }
                    });     
                });
            }     
            
            var listPanel = new AIR2.Search.Facets.ListPanel({
                title   : fp.getFacetLabel(name),
                iconCls : 'air2-icon-'+name,
                activeOnLoad : isActive,
                border  : false,
                defaults: { border: false, style: 'padding: 2px' },
                collapsed: true,
                items   : listOfFacets
            });     
         
            facetPanels.push(listPanel);
    
        });
        
        fp.add(facetPanels);
        fp.doLayout();
    },
        
    getFacetRenderer : function(facetName) {
    
        return null;    // TODO
    },
    
    getFacetLabel : function(facetName) {
        return this.facetDefs[facetName].label;
    },
    
    getItemLabel : function(facetName, item) {
        var label = null;
        var term  = item.term;
        if (Ext.isDefined(item.label)) {
            label = item.label;
        }
        else if (
            Ext.isDefined(this.facetDefs[facetName].itemLabels)
            && 
            Ext.isDefined(this.facetDefs[facetName].itemLabels[term])
        ) {
            label = this.facetDefs[facetName].itemLabels[term];
        }
        else {
            label = term;
        }
        if (!Ext.isDefined(label)) {
            Logger("no label for ",item);
            label = "";
        }
        return label;
    },
    
    // TODO allow for ranges and wildcards
    uriFor : function(facet, term) {
        var filter = facet + '=(' + term + ')';
        if (Ext.isDefined(AIR2.Search.FILTER) && AIR2.Search.FILTER.length) {
            filter += ' ' + AIR2.Search.FILTER;
        }
        var params = Ext.urlEncode({q:AIR2.Search.QUERY, F:filter});
        var uri = this.baseUrl+params;
        return uri;
    }
});

AIR2.Search.Facets.reloadStore = function() {

    // make a copy of the dialect so we don't affect our original
    var dialect = AIR2.Search.Query.toJson();
    var query = new Search.Query.Dialect(dialect);

    // add the original user query
    if (AIR2.Search.PARAMS['q'] !== null) { 
        var clause = new Search.Query.Clause({
            value : AIR2.Search.PARAMS['q']
        });     
        query['and'].push(clause);
    }

    // fetch the store
    var store = AIR2.Search.DATAVIEW.getStore();
    //Logger("store:", store);

    // reload it
    var lastOpts = store.lastOptions;
    Ext.apply(lastOpts.params, {
        q: query.stringify(),
        start: AIR2.Search.getStart(),
        limit: AIR2.Search.getPageSize()
    });
    var facetPanel = Ext.getCmp('air2-search-facets').getEl();
    facetPanel.mask("Applying filter...");
    lastOpts.callback = function() { 
        AIR2.Search.DATAVIEW.refresh();
        facetPanel.unmask();
    };
    store.reload(lastOpts);

}

AIR2.Search.getLocationHash = function() {
    var hashIdx = top.location.href.indexOf("#");
    var urlHash = hashIdx >= 0 ? top.location.href.substr(hashIdx + 1) : '';
    return urlHash;
}

AIR2.Search.setState = function(state) {
    top.location.hash = Ext.urlEncode(state);
}

AIR2.Search.getState = function() {
    var urlHash = AIR2.Search.getLocationHash();
    if (!urlHash.length) {
        return;
    }
    var hashParams = Ext.urlDecode(urlHash);
    var state = {};
    Ext.iterate(hashParams, function(key,val,obj) {
        state[key] = Ext.decode(val);
    });

    return state;
}

AIR2.Search.Facets.getChecked = function() {
    var panel = Ext.getCmp('air2-search-facets').getEl();
    //Logger("facets wrapper:", panel);
    var checkboxes = panel.query('input');
    var checked = [];
    Ext.each(checkboxes, function(item,idx,array) {
        if (item.checked) {
            checked.push(item);
        }
    });
    //Logger('checked:', checked);
    return checked;
}

AIR2.Search.Facets.updateLocation = function() {
    // track which facets are checked via url#target
    var facetState = {};
    var checked = AIR2.Search.Facets.getChecked();
    Ext.each(checked, function(el,idx,array) {
        var cbox = Ext.get(el);
        var label = cbox.next();
        var field = Ext.util.Format.htmlDecode(el.name);
        var value = Ext.util.Format.htmlDecode(label.getAttribute('class'));
        if (!Ext.isDefined(facetState[field])) {
            facetState[field] = {};
        }
        facetState[field][value] = 1;
    });
   
    var state = AIR2.Search.getState(); 
    if (!state) {
        state = {};
    }
    state['facets'] = Ext.encode(facetState);
    AIR2.Search.setState(state);
}

AIR2.Search.Facets.onFacetClick = function(el) {
    //Logger("clicked:",el);
    AIR2.Search.Facets.updateLocation();
}

AIR2.Search.Facets.updateResults = function(domEl) {
    var clause = AIR2.Search.Facets.clauseFromEl(domEl);
    AIR2.Search.Facets.buildQuery(clause, domEl.checked);
    AIR2.Search.Facets.reloadStore();
}

AIR2.Search.Facets.clauseFromEl = function(el) {
    var cbox = Ext.get(el);
    var label = cbox.next();
    //Logger("label:",label.dom);
    var labelText = label.child('.air2-facet-value');
    //Logger("labelText:",labelText.dom);
 
    //make a clause
    var clause = new Search.Query.Clause({
        field   : Ext.util.Format.htmlDecode(el.name),
        value   : Ext.util.Format.htmlDecode(label.getAttribute('class')),
        quote   : label.getAttribute('class').match(/^\(/) ? '' : '"',
        op      : '='
    });
    return clause;
}

AIR2.Search.Facets.buildQuery = function( clause, checked ) {

    var dialect = AIR2.Search.Query;
    
    // are we adding or removing the clause?
    if (checked) {
        //Logger("add Clause");
        
        // if we already have a clause for this field,
        // need to OR them together
        var hasField = false;
        var fieldPrefix;
        var walker = function(c,d,prefix,code) {
            //Logger("query clause:",c.stringify());
            if (!hasField && c.field === clause.field) {
                hasField = true;
                fieldPrefix = prefix;
                //Logger("hasField==TRUE for prefix="+prefix,c.stringify());
            }
        };
        dialect.walk(walker);
        
        if (hasField && fieldPrefix == 'and') {
            //Logger("hasField==true, fieldPrefix="+fieldPrefix);
            var existingAndClause;
            Ext.each(dialect['and'], function(item,idx,array) {
                if (item.field === clause.field) {
                    existingAndClause = item;
                }
            });
           
            if (!existingAndClause) {
                throw new Ext.Error("hasField=true but could not find AND clause for field:"+clause.field); 
            }
            if (existingAndClause.isTree()) {
                existingAndClause.value['or'].push(clause);
            }
            else {
                var combinedClauses = new Search.Query.Dialect({
                    'or'    : [
                        existingAndClause,
                        clause
                    ]
                });
                dialect.removeClause('and',existingAndClause);
                var comboClause = new Search.Query.Clause({
                    op      : '()',
                    value   : combinedClauses,
                    field   : clause.field
                });
                dialect['and'].push(comboClause);
            }
            
        }
        else {
        
        // otherwise, it's a simple AND
            dialect['and'].push(clause);  
          
        }
    }
    else {
        //Logger("remove Clause");
        var branch;
        var walker = function(c,d,prefix,code) {
            //Logger("query clause:",c.stringify());
            if (c.field === clause.field) {
                // now find the value
                if (c.isTree()) {
                    //Logger("found field "+c.field+" where isTree==TRUE",c.stringify());
                    
                    // walk() will next call walker on each child clause,
                    // so we need to identify which one to yank
                    branch = c;
                }
                else {
                    //Logger("simple clause isTree==FALSE",c.stringify());
                    if (branch) {
                        
                        //Logger("clause is branch of tree",c.stringify());
                        if (c.stringify() === clause.stringify()) {
                            //Logger("found branch to trim:",c.stringify());
                            branch.value.removeClause(prefix, c);
                            if (!branch.hasChildren()) {
                                //Logger("branch is now empty; pruning it from dialect");
                                dialect.removeClause('and',branch);
                            } 
                        }
                    }
                    else {
                        dialect.removeClause(prefix, c);
                    }
                }
            }
        };
        dialect.walk(walker);
        
    }

    //Logger("query final:",dialect.toJson());
    //Logger("query final="+dialect);

}

/**********************************************************************
 * @class AIR2.Search.Facets.ListPanel
 * @extends Ext.Panel
 * @returns {AIR2.Search.Facets.ListPanel} panel object
 * 
 * A subclass of Ext.Panel. Each ListPanel should contain
 * an array of AIR2.Search.Facets.Pair objects as its "items."
 */
AIR2.Search.Facets.ListPanel = Ext.extend(Ext.Panel, {

    constructor : function(cfg) {
        var t = cfg.title;
        delete cfg.title;
        cfg.cls = 'air2-facet-panel';
        cfg.header = true;
        cfg.titleCollapse = true;
        cfg.collapsible = true;
        cfg.hideCollapseTool = true;
        cfg.headerCfg = {
            tag: 'div',
            cls: 'air2-facet-title air2-corners',
            html: t
        };
        cfg.iconCls = 'air2-icon-arrow '; 
        cfg.headerCssClass = 'air2-facet-header';
        cfg.headerStyle    = 'border:none;padding:3px;';
        AIR2.Search.Facets.ListPanel.superclass.constructor.call(this,cfg);
    },

    getTitleArrow : function() {
        return this.header.child('img');
    },

    hasCheckedFacets : function() {
        var inputs = this.getEl().query('input');
        if (!inputs.length) {
            return 0;
        }
        var checked = 0;
        Ext.each(inputs, function(item,idx,array) {
            if (item.checked) {
                checked++;
            }
        });
        return checked;
    },

    initComponent : function(cfg) {
    
        if (!Ext.isDefined(this.items)) {
            throw new Ext.Error("items is not defined");
        }

        this.on('beforecollapse', function(lp) {
            //Logger("collapsed", lp.header.dom.innerHTML);
            var arrow = this.getTitleArrow();
            arrow.addClass('air2-icon-arrow-closed');
            arrow.removeClass('air2-icon-arrow-open');
            arrow.removeClass('air2-icon-arrow-closed-hover');
            arrow.removeClass('air2-icon-arrow-open-hover');

            // if there are any checked boxes, indicate via the title bar
            if (lp.hasCheckedFacets()) {
                lp.header.addClass('air2-facet-title-active');
            }

            return true;
        });

        this.on('beforeexpand', function(lp) {
            //Logger("expand", lp.header.dom.innerHTML);
            var arrow = this.getTitleArrow();
            arrow.removeClass('air2-icon-arrow-closed');
            arrow.addClass('air2-icon-arrow-open');
            arrow.removeClass('air2-icon-arrow-closed-hover');
            arrow.removeClass('air2-icon-arrow-open-hover');
            lp.header.removeClass('air2-facet-title-active');
            return true;
        });

        this.on('afterrender', function(lp) {
            if (lp.activeOnLoad) {
                //h.addClass('air2-facet-title-active');
                lp.expand();
                // TODO arrow does not toggle??
                var arrow = this.getTitleArrow();
                arrow.addClass('air2-icon-arrow-open');
                arrow.removeClass('air2-icon-arrow-closed');
            }
        });

        this.on('render', function(lp) {
            var h = lp.header;
            var arrow = lp.getTitleArrow();
            h.on('mouseenter', function(ev,el,obj) {
                h.toggleClass('air2-facet-title-hover');
                if (arrow.hasClass('air2-icon-arrow-closed')) {
                    arrow.addClass('air2-icon-arrow-closed-hover');
                }
                else {
                    arrow.addClass('air2-icon-arrow-open-hover');
                }
            });
            h.on('mouseleave', function(ev,el,obj) {
                h.toggleClass('air2-facet-title-hover');
                arrow.removeClass('air2-icon-arrow-closed-hover');
                arrow.removeClass('air2-icon-arrow-open-hover');
            });        
        });
    
        AIR2.Search.Facets.ListPanel.superclass.initComponent.call(this,cfg);
    }



});

AIR2.Search.cleanQueryForTag = function(str) {
    var clean = str.replace(/[^a-zA-Z0-9\.\ \-\_]\ ?/g, "");
    return clean;
}
